/**
 *  Copyright (C) 2008-2013  Telosys project org. ( http://www.telosys.org/ )
 *
 *  Licensed under the GNU LESSER GENERAL PUBLIC LICENSE, Version 3.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *          http://www.gnu.org/licenses/lgpl.html
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.telosys.persistence.dao;

import java.util.HashMap;
import java.util.List;

import org.telosys.common.TelosysRuntimeException;
import org.telosys.persistence.TelosysPersistenceException;
import org.telosys.persistence.impl.EntityInfoImpl;
import org.telosys.persistence.impl.FieldInfoImpl;
import org.telosys.persistence.mapping.AttributeMapping;
import org.telosys.persistence.mapping.EntityMapping;
import org.telosys.persistence.mapping.LinkMapping;

/**
 * Generic class for entity-table mapping <br>
 * <br>
 * 
 * @author Laurent GUERIN
 *
 * @param <T> the type of the entity 
 * 
 */
public class EntityMapper<T> {

	private final  Class<T> entityClass ;
	
	private final  String   table       ;
	
	private final  String   schema      ;
	
	private FieldMapper[]  keyMappers  ;
	private FieldMapper[]  dataMappers ;
	
	private FieldMapper    autoIncMapper = null ;
	
	private HashMap<String,String>   fieldsToColumns = new HashMap<String,String>() ;
	
	private final  List<LinkMapping> linksToOneMapping ;  // NB : can be null 
	private final  List<LinkMapping> linksToManyMapping ; // NB : can be null 
	
	private SqlRequests<T> sqlRequests   = null ;
	
	/**
	 * Constructor
	 * @param mapping
	 */
	public EntityMapper( EntityMapping<T> mapping ) 
	{
		super();
	
		this.entityClass = mapping.getEntityClass();
		this.table       = mapping.getTable();
		this.schema      = mapping.getSchema() ;
				
		initMapping(entityClass, mapping.getAttributesMapping() );
		
		linksToOneMapping  = mapping.getLinksToOneMapping(); 
		linksToManyMapping = mapping.getLinksToManyMapping();
	}
	
	private void initMapping( Class<T> entityClass, AttributeMapping[] attributesMapping )
    {
		//--- Count
		int numberOfPrimaryKey = 0 ;
		int numberOfData = 0 ;
		for ( AttributeMapping am : attributesMapping ) {
			if ( am != null ) {
				if ( am.isPrimaryKey() ) {
					numberOfPrimaryKey++;
				}
				else {
					numberOfData++;
				}
			}
		}
		
		//--- Build the 2 collections of attributes mappers   
		
		//--- Init the array of Primary Key mappers and the array of Primary Key columns
		keyMappers  = new FieldMapper[numberOfPrimaryKey] ;		
    	String keyColumns[]  = new String[numberOfPrimaryKey];

		int index = 0 ;
    	for ( AttributeMapping am : attributesMapping ) {
			if ( am != null ) {
				FieldMapper fieldMapper = FieldMapperProvider.get(entityClass, index, am);
				if ( am.isPrimaryKey() ) {
					keyMappers[index] = fieldMapper ;
					keyColumns[index] = am.getColumnName() ;
					index++ ;
					
					fieldsToColumns.put( fieldMapper.getAttributeName(), fieldMapper.getDbColumnName() );
				}
			}
		}
    	
		//--- Init the array of Data mappers and the array of Data columns
		dataMappers = new FieldMapper[numberOfData] ; 		
    	String dataColumns[] = new String[numberOfData];
		index = 0 ;
		for ( AttributeMapping am : attributesMapping ) {
			if ( am != null ) {
				FieldMapper fieldMapper = FieldMapperProvider.get(entityClass, index, am);
				if ( am.isPrimaryKey() != true ) {
					dataMappers[index] = fieldMapper ;
					dataColumns[index] = am.getColumnName() ;
					index++ ;

					fieldsToColumns.put( fieldMapper.getAttributeName(), fieldMapper.getDbColumnName() );
				}
			}
		}
		
		chekKeyParts(keyMappers);
		
		initAutoIncMapperIfAny();

		//--- Create the SQL requests
    	if ( this.autoIncMapper != null ) {
    		//--- SQL requests with auto-incremented key
        	this.sqlRequests = new SqlRequests<T>(this.table, this.schema, keyColumns, dataColumns, fieldsToColumns, this.autoIncMapper.getDbColumnName() );
    	}
    	else {
    		//--- Standard SQL requests ( without auto-incremented key )
    		this.sqlRequests = new SqlRequests<T>(this.table, this.schema, keyColumns, dataColumns, fieldsToColumns, null );
    	}
		
    }
	/**
	 * Check the key fields part index consistency 
	 * @param keyFieldMappers
	 */
	private void chekKeyParts(FieldMapper[] keyFieldMappers) {
		//--- If not a composite key : no parts position required
		if ( keyFieldMappers.length < 2 ) return ;
		
    	//--- Array of key elements indexed
    	String[] keyElements = new String[keyFieldMappers.length] ;
        for ( int i = 0 ; i < keyElements.length ; i++ ) {
        	keyElements[i] = null ;
        }
        
        int keyPartCount = 0 ;
        for ( FieldMapper fieldMapper : keyFieldMappers ) {
        	//--- Use KeyPart to get the position of each part 
        	int keyPart = fieldMapper.getKeyPart(); // 1 to N // TODO
        	if ( keyPart != 0 ) {
            	int elementIndex = keyPart-1 ;
            	if ( elementIndex >= 0 && elementIndex < keyElements.length ) {
            		if ( keyElements[elementIndex] != null ) {
            			//--- Already used : ERROR
            			throw new TelosysPersistenceException("Duplicated Key Part #" + keyPart + " (entity '" + entityClass.getName() + "')");
            		}
            		else {
            			//--- Not yet used : OK 
            			keyElements[elementIndex] = fieldMapper.getAttributeName();
            			keyPartCount++ ;
            		}
            	}
            	else {
            		throw new TelosysPersistenceException("Invalid Key Part #" + keyPart + " (entity '" + entityClass.getName() + "')");
            	}
        	}
        }
        if ( keyPartCount > 0 ) {
        	// At least on key part defined => check all key parts are set 
            for ( int i = 0 ; i < keyElements.length ; i++ ) {
            	if ( null == keyElements[i] ) {
            		throw new TelosysPersistenceException("Key Part missing (entity '" + entityClass.getName() + "')");
            	}
            }
        }
	}
	
	public SqlRequests<T> getSqlRequests()
    {
		return this.sqlRequests ;
    }
	
	public Class<T> getEntityClass()
	{
		return this.entityClass ;
	}

	public String getTable()
	{
		return this.table ;
	}
	
	public String getSchema()
	{
		return this.schema ;
	}
	
	/**
	 * Returns the name of the auto-incremented field if any (or null if none)
	 * @return
	 */
	public String getAutoIncrFieldName()
	{
		if ( this.autoIncMapper != null ) {
			return this.autoIncMapper.getAttributeName() ;
		}
		return null ;
	}

	/**
	 * Returns the number of fields defined for the associated class
	 * @return mapped fields count
	 */
	public int getFieldsCount()
	{
		return this.fieldsToColumns.size();
	}
	
	public FieldMapper getKeyFieldMapper(int keyIndex)
	{
		if ( ( keyIndex < 0 ) || ( keyIndex > keyMappers.length ) ) {
			throw new TelosysPersistenceException("Invalid key part index " + keyIndex ) ;
		}
		return keyMappers[keyIndex] ;
	}
	
	/**
	 * Returns the FieldMapper for the given field name (or null if not found)
	 * @param fieldName
	 * @return
	 */
	public FieldMapper getFieldMapper(String fieldName)
	{
		if ( null == fieldName ) throw new TelosysRuntimeException("Field name is null");
        for ( FieldMapper mapper : this.keyMappers ) {
        	if ( fieldName.equals( mapper.getAttributeName() ) ) {
        		return mapper ;
        	}
        }
        for ( FieldMapper mapper : this.dataMappers ) {
        	if ( fieldName.equals( mapper.getAttributeName() ) ) {
        		return mapper ;
        	}
        }
        return null ;
	}
	
	/**
	 * Use the FieldMapper to retrieve the field value
	 * @param entity
	 * @param fieldName
	 * @return
	 */
	public Object getFieldValue(Object entity, String fieldName) {
		FieldMapper fieldMapper = getFieldMapper(fieldName);
		if ( fieldMapper != null ) {
			return fieldMapper.getValue(entity);
		}
		else {
			throw new TelosysRuntimeException("No FieldMapper for field '" + fieldName + "' in " + entityClass );
		}
	}
	
    private void initAutoIncMapperIfAny() //throws TelosysException
    {
    	//--- In PRIMARY KEY parts 
    	this.autoIncMapper = null ;
        for ( FieldMapper mapper : this.keyMappers ) {
        	if ( mapper.isAutoIncrementedColumn() ) {
        		if ( this.autoIncMapper != null ) {
        			throw new TelosysRuntimeException("Multiple auto-incremented attributes detected.");
        		}
        		else {
            		this.autoIncMapper = mapper ;
        		}
        	}
        }
    	//--- In DATA parts 
        for ( FieldMapper mapper : this.dataMappers ) {
        	if ( mapper.isAutoIncrementedColumn() ) {
    			throw new TelosysRuntimeException("Auto-incremented attribute allowed only in Primary Key");
        	}
        }
    }
    
    public void setPrimaryKey( T entity, QueryContext queryContext ) //throws TelosysException
    {
        int index = 0;
        //--- Primary Key ( set the Primary Key columns for the SQL WHERE criteria )
        for ( FieldMapper mapper : keyMappers ) {
        	mapper.beanToTable(queryContext, entity, ++index) ;
        }
    }
    
    public void tableToBean( T entity, QueryContext queryContext ) //throws TelosysException
    {
//        //--- Data ( without the Primary Key )
//        for ( AttributeMapper mapper : dataColumnsMapping ) {
//        	mapper.tableToBean(queryContext, entity) ;
//        }
//        //--- Primary Key
//        for ( AttributeMapper mapper : keyColumnsMapping ) {
//        	mapper.tableToBean(queryContext, entity) ;
//        }
        int index = 0 ;
    	//--- 1rst : Data attributes ( to conform with SQL request columns order )
        for ( FieldMapper mapper : dataMappers ) {
        	mapper.tableToBean(queryContext, entity, ++index) ;
        }
    	//--- 2nd : Primary Key attributes ( to conform with SQL request columns order )
        for ( FieldMapper mapper : keyMappers ) {
        	mapper.tableToBean(queryContext, entity, ++index) ;
        }
    }

    public void beanToTable( T entity, QueryContext queryContext ) //throws TelosysException
    {
//        //--- Data ( without the Primary Key )
//        for ( AttributeMapper mapper : dataColumnsMapping ) {
//        	mapper.beanToTable(queryContext, entity) ;
//        }
//        //--- Primary Key
//        for ( AttributeMapper mapper : keyColumnsMapping ) {
//        	mapper.beanToTable(queryContext, entity) ;
//        }

        int index = 0 ;
    	//--- 1rst : Data attributes ( to conform with SQL request columns order )
    	for ( FieldMapper mapper : dataMappers ) {
        	mapper.beanToTable(queryContext, entity, ++index) ;
        }
    	//--- 2nd : Primary Key attributes ( to conform with SQL request columns order )
    	for ( FieldMapper mapper : keyMappers ) {
        	mapper.beanToTable(queryContext, entity, ++index) ;
        }
    }
    
    public void beanToTableWithoutAutoInc ( T entity, QueryContext queryContext ) //throws TelosysException
    {
//        //--- Data ( without the Primary Key )
//        for ( AttributeMapper mapper : dataColumnsMapping ) {
//        	if ( mapper.isAutoIncrementedColumn() != true ) {
//            	mapper.beanToTable(queryContext, entity) ;
//        	}
//        }        
//        //--- Primary Key
//        for ( AttributeMapper mapper : keyColumnsMapping ) {
//        	if ( mapper.isAutoIncrementedColumn() != true ) {
//            	mapper.beanToTable(queryContext, entity) ;
//        	}
//        }

        int index = 0 ;
    	//--- 1rst : Data attributes ( to conform with SQL request columns order )
    	for ( FieldMapper mapper : dataMappers ) {
        	mapper.beanToTable(queryContext, entity, ++index) ;
        }
    	//--- 2nd : Primary Key attributes ( to conform with SQL request columns order )
        for ( FieldMapper mapper : keyMappers ) {
        	if ( mapper.isAutoIncrementedColumn() != true ) {
            	mapper.beanToTable(queryContext, entity, ++index) ;
        	}
        }
    }
    
    public void setAutoIncrementedValue(T entity, long value) //throws TelosysException 
	{
		if ( autoIncMapper != null ) {
			autoIncMapper.setAutoIncrementedValue(entity, value);
		}
		else {
			throw new TelosysPersistenceException("No auto-incremented column for this entity ("+ entityClass.getName() + ")" );
		}
	}
    
    public T createEntityWithPK(Object ... primaryKeyValues ) //throws TelosysException
	{
        if ( keyMappers.length < 1 ) {
        	throw new TelosysPersistenceException("No primary key for class " + entityClass.getName() );
        }
		if ( null == primaryKeyValues ) {
			throw new TelosysPersistenceException("Primary Key value(s) required (entity '" + entityClass.getName() + "')");
		}
		if ( primaryKeyValues.length != keyMappers.length ) {
			throw new TelosysPersistenceException("Invalid Primary Key, must have " + keyMappers.length + " values (entity '" + entityClass.getName() + "')");
		}
		
		//--- New entity instance
		T entity = null ;
        try
        {
    		entity = entityClass.newInstance();
        } catch (Throwable t)
        {
        	throw new TelosysPersistenceException("Cannot create instance for class " + entityClass.getName() );
        }

		//--- Primary key initialization        
        if ( keyMappers.length == 1 ) {
        	//--- The primary key has only 1 part
        	FieldMapper mapper = keyMappers[0];
        	mapper.setValue(entity, primaryKeyValues[0] );
        }
        else {
        	//--- The primary key is composed of N parts 
            for ( FieldMapper mapper : keyMappers ) {
            	//--- Use KeyPart to get the position of each part 
            	int keyPart = mapper.getKeyPart(); // 1 to N // TODO
            	int index = keyPart-1 ;
            	if ( index < 0 ) {
            		throw new TelosysPersistenceException("Undefined Key Part for composite Primary Key (entity '" + entityClass.getName() + "')");
            	}
            	if ( index >= primaryKeyValues.length ) {
            		throw new TelosysPersistenceException("No value for Key Part #" + keyPart + " (entity '" + entityClass.getName() + "')");
            	}
               	mapper.setValue(entity, primaryKeyValues[index] );
            }
        }        	

        return entity ;
	}

	@Override
	public String toString() 
	{
		
		StringBuffer sb = new StringBuffer();
		sb.append("Mapper for " );
		sb.append( entityClass != null ? entityClass.getSimpleName() : "No entity class !") ;
		sb.append(" : table = '" );
		sb.append( this.table ) ;
		sb.append("', schema = '" );
		sb.append( this.schema ) ;
		sb.append("', " );
		sb.append(keyMappers != null ? keyMappers.length : 0 );
		sb.append(" key field(s)" );
		sb.append("', " );
		sb.append(dataMappers != null ? dataMappers.length : 0 );
		sb.append(" data field(s), " );
		sb.append("auto-incr field : " + getAutoIncrFieldName() );
		
		return sb.toString();
	}
    
	private FieldInfoImpl toFieldInfo( FieldMapper fieldMapper ) {
		return new FieldInfoImpl(
				fieldMapper.getAttributeName(), 
				fieldMapper.getDbColumnName(), 
				fieldMapper.isPrimaryKey(),
				fieldMapper.isAutoIncrementedColumn(),
				fieldMapper.getKeyPart());
	}
	
	/**
	 * Returns entity mapping information
	 * @return
	 */
	public EntityInfoImpl toEntityInfo() {
		
		FieldInfoImpl[] keyFieldsInfo = new FieldInfoImpl[keyMappers.length] ;		
		for ( int i = 0 ; i < keyMappers.length ; i++ ) {
			keyFieldsInfo[i] = toFieldInfo( keyMappers[i] );
		}
		FieldInfoImpl[] dataFieldsInfo = new FieldInfoImpl[dataMappers.length] ;		
		for ( int i = 0 ; i < dataMappers.length ; i++ ) {
			dataFieldsInfo[i] = toFieldInfo( dataMappers[i] );
		}
				
		EntityInfoImpl entityInfo = new EntityInfoImpl(entityClass, table, schema, keyFieldsInfo, dataFieldsInfo);
		
		return entityInfo ;
	}
	
    //-------------------------------------------------------------------------------------------------------------------
	// LINK "TO ONE"
    //-------------------------------------------------------------------------------------------------------------------
    private LinkMapping getLinkToOneMappingByType(Class<?> linkClass ) 
    {
    	return findLinkMappingByType( this.linksToOneMapping, linkClass ); 
//    	if ( this.linksToOneMapping != null ) {
//    		LinkMapping linkMappingFound = null ;
//        	for ( LinkMapping linkMapping : this.linksToOneMapping ) {
//        		Class<?> type = linkMapping.getType() ;
//        		if ( type.getCanonicalName().equals( linkClass.getCanonicalName() ) ) {
//        			// Found
//        			if ( null == linkMappingFound ) {
//            			linkMappingFound = linkMapping ;
//        			}
//        			else {
//        				throw new TelosysPersistenceException("Ambiguous, multiple links for class " + linkClass.getSimpleName() + " (use link name)" );
//        			}
//        		}
//        	}
//        	return linkMappingFound ;
//    	}
//    	return null ;
    }
    //-------------------------------------------------------------------------------------------------------------------
    private LinkMapping getLinkToOneMappingByName(Class<?> linkClass, String linkName ) 
    {
    	return findLinkMappingByName( this.linksToOneMapping, linkClass, linkName ) ;
    }
    //-------------------------------------------------------------------------------------------------------------------
    public Object[] getLinkToOneKeyParts(Object entity, Class<?> linkClass ) 
    {
    	LinkMapping linkMapping = getLinkToOneMappingByType( linkClass ) ;
    	if ( linkMapping != null ) {
    		return getKeyPartsValues( entity, linkMapping ) ;
    	}
    	else {
    		throw new TelosysPersistenceException("No link from '" + entity.getClass().getSimpleName() 
    				+ "' to '" + linkClass.getSimpleName() + "'");
    	}
    }
    //-------------------------------------------------------------------------------------------------------------------
    public Object[] getLinkToOneKeyParts(Object entity, Class<?> linkClass, String linkName ) 
    {
    	LinkMapping linkMapping = getLinkToOneMappingByName( linkClass, linkName ) ;
    	if ( linkMapping != null ) {
        	return getKeyPartsValues( entity, linkMapping ) ;
    	}
    	else {
    		throw new TelosysPersistenceException("No link with name '" + linkName + "' in entity '" + entity.getClass().getSimpleName() + "'"); 
    	}
    }
    //-------------------------------------------------------------------------------------------------------------------
    private Object[] getKeyPartsValues( Object entity, LinkMapping linkMapping ) 
    {
    	String[] keyFields = linkMapping.getKeyParts();
    	Object[] keyParts = new Object[keyFields.length] ;
    	for ( int i = 0 ; i < keyFields.length ; i++ ) {
    		String fieldName = keyFields[i] ;
    		//--- Get the field value
    		keyParts[i] = this.getFieldValue(entity, fieldName) ;
    	}
    	return keyParts ;
    }
	
    //-------------------------------------------------------------------------------------------------------------------
	// LINK "TO MANY"
    //-------------------------------------------------------------------------------------------------------------------
    public LinkMapping getLinkToManyMappingByType(Class<?> linkClass ) 
    {
    	return findLinkMappingByType( this.linksToManyMapping, linkClass ); 
    }
    public LinkMapping getLinkToManyMappingByName(Class<?> linkClass, String linkName ) 
    {
    	return findLinkMappingByName( this.linksToManyMapping, linkClass, linkName ) ;
    }    
    //-------------------------------------------------------------------------------------------------------------------
	// LINK MAPPING FINDERS
    //-------------------------------------------------------------------------------------------------------------------
    /**
     * Try to find a LinkMapping by the link type
     * @param linksMapping
     * @param linkClass
     * @return
     */
    private LinkMapping findLinkMappingByType( List<LinkMapping> linksMapping, Class<?> linkClass ) 
    {
    	if ( linksMapping != null ) {
    		LinkMapping linkMappingFound = null ;
        	for ( LinkMapping linkMapping : linksMapping ) {
        		Class<?> type = linkMapping.getType() ;
        		if ( type.getCanonicalName().equals( linkClass.getCanonicalName() ) ) {
        			// Found
        			if ( null == linkMappingFound ) {
            			linkMappingFound = linkMapping ;
        			}
        			else {
        				throw new TelosysPersistenceException("Ambiguous, multiple links for class " + linkClass.getSimpleName() + " (use link name)" );
        			}
        		}
        	}
        	return linkMappingFound ;
    	}
    	return null ;
    }
    //-------------------------------------------------------------------------------------------------------------------
    /**
     * Try to find a LinkMapping by the link name
     * @param linksMapping
     * @param linkClass
     * @param linkName
     * @return
     */
    private LinkMapping findLinkMappingByName( List<LinkMapping> linksMapping, Class<?> linkClass, String linkName ) 
    {
    	if ( linksMapping != null ) {
    		LinkMapping linkMappingFound = null ;
        	for ( LinkMapping linkMapping : linksMapping ) {
        		String name = linkMapping.getName() ;
        		if ( name.equals( linkName ) ) {
        			// Found
        			if ( null == linkMappingFound ) {
            			linkMappingFound = linkMapping ;
        			}
        			else {
        				throw new TelosysPersistenceException("Multiple links with name '" + linkName + "'" );
        			}
        		}
        	}
        	if ( linkMappingFound != null ) {
            	if ( linkMappingFound.getType().getCanonicalName().equals( linkClass.getCanonicalName() ) ) {
                	return linkMappingFound ;
            	}
            	else {
    				throw new TelosysPersistenceException("Type of link '" + linkName + "' is not " + linkClass.getCanonicalName() );
            	}
        	}
    	}
    	return null ;
    }
    //-------------------------------------------------------------------------------------------------------------------
}
